package com.mcxiaoke.minicat.util;

/**
 * MultiTouchController.java
 *
 * Author: Luke Hutchison (luke.hutch@mit.edu)
 *   Please drop me an email if you use this code so I can list your project here!
 *
 * Usage:
 *   <code>
 *   public class MyMTView extends View implements MultiTouchObjectCanvas<PinchWidgetType> {
 *
 *       private MultiTouchController<PinchWidgetType> multiTouchController = new MultiTouchController<PinchWidgetType>(this);
 *
 *       // Pass touch events to the MT controller
 *       public boolean onTouchEvent(MotionEvent event) {
 *           return multiTouchController.onTouchEvent(event);
 *       }
 *
 *       // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface.
 *   }
 *   </code>
 *
 * Changelog:
 *   2010-06-09 v1.5.1  Some API changes to make it possible to selectively update or not update scale / rotation.
 *                      Fixed anisotropic zoom.  Cleaned up rotation code.  Added more comments.  Better var names. (LH)
 *   2010-06-09 v1.4    Added ability to track pinch rotation (Mickael Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH)
 *   2010-06-09 v1.3.3  Bugfixes for Android-2.1; added optional debug info (LH)
 *   2010-06-09 v1.3    Ported to Android-2.2 (handle ACTION_POINTER_* actions); fixed several bugs; refactoring; documentation (LH) 
 *   2010-05-17 v1.2.1  Dual-licensed under Apache and GPL licenses
 *   2010-02-18 v1.2    Support for compilation under Android 1.5/1.6 using introspection (mmin, author of handyCalc)
 *   2010-01-08 v1.1.1  Bugfixes to Cyanogen's patch that only showed up in more complex uses of controller (LH) 
 *   2010-01-06 v1.1    Modified for official level 5 MT API (Cyanogen)
 *   2009-01-25 v1.0    Original MT controller, released for hacked G1 kernel (LH) 
 *
 * Planned features:
 * - Add inertia (flick-pinch-zoom or flick-scroll)
 *
 * Known usages:
 * - Mickael Despesse's "Face Frenzy" face distortion app, to be published to the Market soon
 * - Yuan Chin's fork of ADW Launcher to support multitouch
 * - David Byrne's fractal viewing app Fractoid
 * - mmin's handyCalc calculator
 * - My own "MultiTouch Visualizer 2" in the Market
 * - Formerly: The browser in cyanogenmod (and before that, JesusFreke), and other firmwares like dwang5.  This usage has been
 *   replaced with official pinch/zoom in Maps, Browser and Gallery[3D] as of API level 5.
 *
 * License:
 *   Dual-licensed under the Apache License v2 and the GPL v2.
 */

import android.util.Log;
import android.view.MotionEvent;

import java.lang.reflect.Method;

/**
 * A class that simplifies the implementation of multitouch in applications.
 * Subclass this and read the fields here as needed in subclasses.
 *
 * @author Luke Hutchison
 */
public class MultiTouchController<T> {

    /**
     * The max number of touch points that can be present on the screen at once
     */
    public static final int MAX_TOUCH_POINTS = 20;
    private static final float[] xVals = new float[MAX_TOUCH_POINTS];
    private static final float[] yVals = new float[MAX_TOUCH_POINTS];
    private static final float[] pressureVals = new float[MAX_TOUCH_POINTS];
    private static final int[] pointerIds = new int[MAX_TOUCH_POINTS];
    /**
     * Generate tons of log entries for debugging
     */
    public static final boolean DEBUG = false;

    // ----------------------------------------------------------------------------------------------------------------------
    public static final boolean multiTouchSupported;
    /**
     * Time in ms required after a change in event status (e.g. putting down or
     * lifting off the second finger) before events actually do anything --
     * helps eliminate noisy jumps that happen on change of status
     */
    private static final long EVENT_SETTLE_TIME_INTERVAL = 20;
    /**
     * The biggest possible abs val of the change in x or y between multitouch
     * events (larger dx/dy events are ignored) -- helps eliminate jumps in
     * pointer position on finger 2 up/down.
     */
    private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f;
    /**
     * The biggest possible abs val of the change in multitouchWidth or
     * multitouchHeight between multitouch events (larger-jump events are
     * ignored) -- helps eliminate jumps in pointer position on finger 2
     * up/down.
     */
    private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f;
    /**
     * The smallest possible distance between multitouch points (used to avoid
     * div-by-zero errors and display glitches)
     */
    private static final float MIN_MULTITOUCH_SEPARATION = 30.0f;

    // ----------------------------------------------------------------------------------------------------------------------
    /**
     * No touch points down.
     */
    private static final int MODE_NOTHING = 0;
    /**
     * Current drag mode
     */
    private int mMode = MODE_NOTHING;
    /**
     * One touch point down, dragging an object.
     */
    private static final int MODE_DRAG = 1;
    /**
     * Two or more touch points down, stretching/rotating an object using the
     * first two touch points.
     */
    private static final int MODE_PINCH = 2;
    private static Method m_getPointerCount;
    private static Method m_getPointerId;
    private static Method m_getPressure;

    // ----------------------------------------------------------------------------------------------------------------------
    private static Method m_getHistoricalX;
    private static Method m_getHistoricalY;
    private static Method m_getHistoricalPressure;
    private static Method m_getX;

    // ----------------------------------------------------------------------------------------------------------------------
    private static Method m_getY;
    private static int ACTION_POINTER_UP = 6;

    // ------------------------------------------------------------------------------------
    private static int ACTION_POINTER_INDEX_SHIFT = 8;
    static {
        boolean succeeded = false;
        try {
            // Android 2.0.1 stuff:
            m_getPointerCount = MotionEvent.class.getMethod("getPointerCount");
            m_getPointerId = MotionEvent.class.getMethod("getPointerId",
                    Integer.TYPE);
            m_getPressure = MotionEvent.class.getMethod("getPressure",
                    Integer.TYPE);
            m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX",
                    Integer.TYPE, Integer.TYPE);
            m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY",
                    Integer.TYPE, Integer.TYPE);
            m_getHistoricalPressure = MotionEvent.class.getMethod(
                    "getHistoricalPressure", Integer.TYPE, Integer.TYPE);
            m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE);
            m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE);
            succeeded = true;
        } catch (Exception e) {
            Log.e("MultiTouchController", "static initializer failed", e);
        }
        multiTouchSupported = succeeded;
        if (multiTouchSupported) {
            // Android 2.2+ stuff (the original Android 2.2 consts are declared
            // above,
            // and these actions aren't used previous to Android 2.2):
            try {
                ACTION_POINTER_UP = MotionEvent.class.getField(
                        "ACTION_POINTER_UP").getInt(null);
                ACTION_POINTER_INDEX_SHIFT = MotionEvent.class.getField(
                        "ACTION_POINTER_INDEX_SHIFT").getInt(null);
            } catch (Exception e) {
            }
        }
    }

    // ------------------------------------------------------------------------------------
    MultiTouchObjectCanvas<T> objectCanvas;
    /**
     * The current touch point
     */
    private PointInfo mCurrPt;
    /**
     * The previous touch point
     */
    private PointInfo mPrevPt;
    /**
     * Fields extracted from mCurrPt
     */
    private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight,
            mCurrPtAng;
    /**
     * Whether to handle single-touch events/drags before multi-touch is
     * initiated or not; if not, they are handled by subclasses
     */
    private boolean handleSingleTouchEvents;
    /**
     * The object being dragged/stretched
     */
    private T selectedObject = null;
    /**
     * Current position and scale of the dragged object
     */
    private PositionAndScale mCurrXform = new PositionAndScale();
    /**
     * Drag/pinch check time and time to ignore spurious events until (to smooth
     * over event noise)
     */
    private long mSettleStartTime, mSettleEndTime;
    /**
     * Conversion from object coords to screen coords
     */
    private float startPosX, startPosY;
    /**
     * Conversion between scale and width, and object angle and check pinch
     * angle
     */
    private float startScaleOverPinchDiam, startAngleMinusPinchAngle;
    /**
     * Conversion between X scale and width, and Y scale and height
     */
    private float startScaleXOverPinchWidth, startScaleYOverPinchHeight;

    /**
     * Constructor that sets handleSingleTouchEvents to true
     */
    public MultiTouchController(MultiTouchObjectCanvas<T> objectCanvas) {
        this(objectCanvas, true);
    }

    // ------------------------------------------------------------------------------------

    /**
     * Full constructor
     */
    public MultiTouchController(MultiTouchObjectCanvas<T> objectCanvas,
                                boolean handleSingleTouchEvents) {
        this.mCurrPt = new PointInfo();
        this.mPrevPt = new PointInfo();
        this.handleSingleTouchEvents = handleSingleTouchEvents;
        this.objectCanvas = objectCanvas;
    }

    /**
     * Extract fields from mCurrPt, respecting the update* fields of mCurrPt.
     * This just avoids code duplication. I hate that Java doesn't support
     * higher-order functions, tuples or multiple return values from functions.
     */
    private void extractCurrPtInfo() {
        // Get new drag/pinch params. Only read multitouch fields that are
        // needed,
        // to avoid unnecessary computation (diameter and angle are expensive
        // operations).
        mCurrPtX = mCurrPt.getX();
        mCurrPtY = mCurrPt.getY();
        mCurrPtDiam = Math.max(
                MIN_MULTITOUCH_SEPARATION * .71f,
                !mCurrXform.updateScale ? 0.0f : mCurrPt
                        .getMultiTouchDiameter());
        mCurrPtWidth = Math
                .max(MIN_MULTITOUCH_SEPARATION,
                        !mCurrXform.updateScaleXY ? 0.0f : mCurrPt
                                .getMultiTouchWidth());
        mCurrPtHeight = Math.max(
                MIN_MULTITOUCH_SEPARATION,
                !mCurrXform.updateScaleXY ? 0.0f : mCurrPt
                        .getMultiTouchHeight());
        mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt
                .getMultiTouchAngle();
    }

    /**
     * Whether to handle single-touch events/drags before multi-touch is
     * initiated or not; if not, they are handled by subclasses. Default: true
     */
    protected boolean getHandleSingleTouchEvents() {
        return handleSingleTouchEvents;
    }

    /**
     * Whether to handle single-touch events/drags before multi-touch is
     * initiated or not; if not, they are handled by subclasses. Default: true
     */
    protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) {
        this.handleSingleTouchEvents = handleSingleTouchEvents;
    }

    /**
     * Process incoming touch events
     */
    public boolean onTouchEvent(MotionEvent event) {
        try {
            int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount
                    .invoke(event) : 1;
            if (DEBUG)
                Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " "
                        + mMode + " " + handleSingleTouchEvents + " "
                        + pointerCount);
            if (mMode == MODE_NOTHING && !handleSingleTouchEvents
                    && pointerCount == 1)
                // Not handling initial single touch events, just pass them on
                return false;
            if (DEBUG)
                Log.i("MultiTouch", "Got here 2");

            // Handle history first (we sometimes get history with ACTION_MOVE
            // events)
            int action = event.getAction();
            int histLen = event.getHistorySize() / pointerCount;
            for (int histIdx = 0; histIdx <= histLen; histIdx++) {
                // Read from history entries until histIdx == histLen, then read
                // from current event
                boolean processingHist = histIdx < histLen;
                if (!multiTouchSupported || pointerCount == 1) {
                    // Use single-pointer methods -- these are needed as a
                    // special case (for some weird reason) even if
                    // multitouch is supported but there's only one touch point
                    // down currently -- event.getX(0) etc. throw
                    // an exception if there's only one point down.
                    if (DEBUG)
                        Log.i("MultiTouch", "Got here 3");
                    xVals[0] = processingHist ? event.getHistoricalX(histIdx)
                            : event.getX();
                    yVals[0] = processingHist ? event.getHistoricalY(histIdx)
                            : event.getY();
                    pressureVals[0] = processingHist ? event
                            .getHistoricalPressure(histIdx) : event
                            .getPressure();
                } else {
                    // Read x, y and pressure of each pointer
                    if (DEBUG)
                        Log.i("MultiTouch", "Got here 4");
                    int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS);
                    for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) {
                        int ptrId = (Integer) m_getPointerId.invoke(event,
                                ptrIdx);
                        pointerIds[ptrIdx] = ptrId;
                        // N.B. if pointerCount == 1, then the following methods
                        // throw an array index out of range exception,
                        // and the code above is therefore required not just for
                        // Android 1.5/1.6 but also for when there is
                        // only one touch point on the screen -- pointlessly
                        // inconsistent :(
                        xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX
                                .invoke(event, ptrIdx, histIdx) : m_getX
                                .invoke(event, ptrIdx));
                        yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY
                                .invoke(event, ptrIdx, histIdx) : m_getY
                                .invoke(event, ptrIdx));
                        pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure
                                .invoke(event, ptrIdx, histIdx) : m_getPressure
                                .invoke(event, ptrIdx));
                    }
                }
                // Decode event
                decodeTouchEvent(
                        pointerCount,
                        xVals,
                        yVals,
                        pressureVals,
                        pointerIds, //
                        /* action = */processingHist ? MotionEvent.ACTION_MOVE
                                : action, //
						/* down = */processingHist ? true
                                : action != MotionEvent.ACTION_UP //
                                && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP //
                                && action != MotionEvent.ACTION_CANCEL, //
                        processingHist ? event.getHistoricalEventTime(histIdx)
                                : event.getEventTime());
            }

            return true;
        } catch (Exception e) {
            // In case any of the introspection stuff fails (it shouldn't)
            Log.e("MultiTouchController", "onTouchEvent() failed", e);
            return false;
        }
    }

    private void decodeTouchEvent(int pointerCount, float[] x, float[] y,
                                  float[] pressure, int[] pointerIds, int action, boolean down,
                                  long eventTime) {
        if (DEBUG)
            Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action
                    + " " + down);

        // Swap curr/prev points
        PointInfo tmp = mPrevPt;
        mPrevPt = mCurrPt;
        mCurrPt = tmp;
        // Overwrite old prev point
        mCurrPt.set(pointerCount, x, y, pressure, pointerIds, action, down,
                eventTime);
        multiTouchController();
    }

    // ------------------------------------------------------------------------------------

    /**
     * Start dragging/pinching, or reset drag/pinch to current point if
     * something goes out of range
     */
    private void anchorAtThisPositionAndScale() {
        if (selectedObject == null)
            return;

        // Get selected object's current position and scale
        objectCanvas.getPositionAndScale(selectedObject, mCurrXform);

        // Figure out the object coords of the drag check point's screen coords.
        // All stretching should be around this point in object-coord-space.
        // Also figure out out ratio between object scale factor and multitouch
        // diameter at beginning of drag; same for angle and optional
        // anisotropic
        // scale.
        float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f
                : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale);
        extractCurrPtInfo();
        startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv;
        startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv;
        startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam;
        startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth;
        startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight;
        startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng;
    }

    /**
     * Drag/stretch/rotate the selected object using the current touch
     * position(s) relative to the anchor position(s).
     */
    private void performDragOrPinch() {
        // Don't do anything if we're not dragging anything
        if (selectedObject == null)
            return;

        // Calc new position of dragged object
        float currScale = !mCurrXform.updateScale ? 1.0f
                : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale;
        extractCurrPtInfo();
        float newPosX = mCurrPtX - startPosX * currScale;
        float newPosY = mCurrPtY - startPosY * currScale;
        float newScale = startScaleOverPinchDiam * mCurrPtDiam;
        float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth;
        float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight;
        float newAngle = startAngleMinusPinchAngle + mCurrPtAng;

        // Set the new obj coords, scale, and angle as appropriate (notifying
        // the subclass of the change).
        mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY,
                newAngle);

        boolean success = objectCanvas.setPositionAndScale(selectedObject,
                mCurrXform, mCurrPt);
        if (!success)
            ; // If we could't set those params, do nothing currently
    }

    /**
     * State-based controller for tracking switches between no-touch,
     * single-touch and multi-touch situations. Includes logic for cleaning up
     * the event stream, as events around touch up/down are noisy at least on
     * early Synaptics sensors.
     */
    private void multiTouchController() {
        if (DEBUG)
            Log.i("MultiTouch",
                    "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints()
                            + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch());

        switch (mMode) {
            case MODE_NOTHING:
                // Not doing anything currently
                if (mCurrPt.isDown()) {
                    // Start a new single-point drag
                    selectedObject = objectCanvas
                            .getDraggableObjectAtPoint(mCurrPt);
                    if (selectedObject != null) {
                        // Started a new single-point drag
                        mMode = MODE_DRAG;
                        objectCanvas.selectObject(selectedObject, mCurrPt);
                        anchorAtThisPositionAndScale();
                        // Don't need any settling time if just placing one finger,
                        // there is no noise
                        mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime();
                    }
                }
                break;

            case MODE_DRAG:
                // Currently in a single-point drag
                if (!mCurrPt.isDown()) {
                    // First finger was released, stop dragging
                    mMode = MODE_NOTHING;
                    objectCanvas.selectObject((selectedObject = null), mCurrPt);

                } else if (mCurrPt.isMultiTouch()) {
                    // Point 1 was already down and point 2 was just placed down
                    mMode = MODE_PINCH;
                    // Restart the drag with the new drag position (that is at the
                    // midpoint between the touchpoints)
                    anchorAtThisPositionAndScale();
                    // Need to let events settle before moving things, to help with
                    // event noise on touchdown
                    mSettleStartTime = mCurrPt.getEventTime();
                    mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL;

                } else {
                    // Point 1 is still down and point 2 did not change state, just
                    // do single-point drag to new location
                    if (mCurrPt.getEventTime() < mSettleEndTime) {
                        // Ignore the first few events if we just stopped
                        // stretching, because if finger 2 was kept down while
                        // finger 1 is lifted, then point 1 gets mapped to finger 2.
                        // Restart the drag from the new position.
                        anchorAtThisPositionAndScale();
                    } else {
                        // Keep dragging, move to new point
                        performDragOrPinch();
                    }
                }
                break;

            case MODE_PINCH:
                // Two-point pinch-scale/rotate/translate
                if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) {
                    // Dropped one or both points, stop stretching

                    if (!mCurrPt.isDown()) {
                        // Dropped both points, go back to doing nothing
                        mMode = MODE_NOTHING;
                        objectCanvas.selectObject((selectedObject = null), mCurrPt);

                    } else {
                        // Just dropped point 2, downgrade to a single-point drag
                        mMode = MODE_DRAG;
                        // Restart the pinch with the single-finger position
                        anchorAtThisPositionAndScale();
                        // Ignore the first few events after the drop, in case we
                        // dropped finger 1 and left finger 2 down
                        mSettleStartTime = mCurrPt.getEventTime();
                        mSettleEndTime = mSettleStartTime
                                + EVENT_SETTLE_TIME_INTERVAL;
                    }

                } else {
                    // Still pinching
                    if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE
                            || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE
                            || Math.abs(mCurrPt.getMultiTouchWidth()
                            - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE
                            || Math.abs(mCurrPt.getMultiTouchHeight()
                            - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) {
                        // Jumped too far, probably event noise, reset and ignore
                        // events for a bit
                        anchorAtThisPositionAndScale();
                        mSettleStartTime = mCurrPt.getEventTime();
                        mSettleEndTime = mSettleStartTime
                                + EVENT_SETTLE_TIME_INTERVAL;

                    } else if (mCurrPt.eventTime < mSettleEndTime) {
                        // Events have not yet settled, reset
                        anchorAtThisPositionAndScale();
                    } else {
                        // Stretch to new position and size
                        performDragOrPinch();
                    }
                }
                break;
        }
        if (DEBUG)
            Log.i("MultiTouch",
                    "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints()
                            + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch());
    }

    // ------------------------------------------------------------------------------------

    public static interface MultiTouchObjectCanvas<T> {

        /**
         * See if there is a draggable object at the current point. Returns the
         * object at the point, or null if nothing to drag. To check a
         * multitouch drag/stretch operation, this routine must return some
         * non-null reference to an object. This object is passed into the other
         * methods in this interface when they are called.
         *
         * @param touchPoint The point being tested (in object coordinates). Return the
         *                   topmost object under this point, or if dragging/stretching
         *                   the whole canvas, just return a reference to the canvas.
         * @return a reference to the object under the point being tested, or
         * null to cancel the drag operation. If dragging/stretching the
         * whole canvas (e.g. in a photo viewer), always return
         * non-null, otherwise the stretch operation won't work.
         */
        public T getDraggableObjectAtPoint(PointInfo touchPoint);

        /**
         * Get the screen coords of the dragged object's origin, and scale
         * multiplier to convert screen coords to obj coords. The job of this
         * routine is to call the .set() method on the passed PositionAndScale
         * object to record the initial position and scale of the object (in
         * object coordinates) before any dragging/stretching takes place.
         *
         * @param obj               The object being dragged/stretched.
         * @param objPosAndScaleOut Output parameter: You need to call objPosAndScaleOut.set()
         *                          to record the current position and scale of obj.
         */
        public void getPositionAndScale(T obj,
                                        PositionAndScale objPosAndScaleOut);

        /**
         * Callback to update the position and scale (in object coords) of the
         * currently-dragged object.
         *
         * @param obj               The object being dragged/stretched.
         * @param newObjPosAndScale The new position and scale of the object, in object
         *                          coordinates. Use this to move/resize the object before
         *                          returning.
         * @param touchPoint        Info about the current touch point, including multitouch
         *                          information and utilities to calculate and cache
         *                          multitouch pinch diameter etc. (Note: touchPoint is
         *                          volatile, if you want to keep any fields of touchPoint,
         *                          you must copy them before the method body exits.)
         * @return true if setting the position and scale of the object was
         * successful, or false if the position or scale parameters are
         * out of range for this object.
         */
        public boolean setPositionAndScale(T obj,
                                           PositionAndScale newObjPosAndScale, PointInfo touchPoint);

        /**
         * Select an object at the given point. Can be used to bring the object
         * to top etc. Only called when first touchpoint goes down, not when
         * multitouch is initiated. Also called with null on touch-up.
         *
         * @param obj        The object being selected by single-touch, or null on
         *                   touch-up.
         * @param touchPoint The current touch point.
         */
        public void selectObject(T obj, PointInfo touchPoint);
    }

    // ------------------------------------------------------------------------------------

    /**
     * A class that packages up all MotionEvent information with all derived
     * multitouch information (if available)
     */
    public static class PointInfo {
        // Multitouch information
        private int numPoints;
        // Midpoint of pinch operations
        private float xMid, yMid, pressureMid;        private float[] xs = new float[MAX_TOUCH_POINTS];
        // Width/diameter/angle of pinch operations
        private float dx, dy, diameter, diameterSq, angle;        private float[] ys = new float[MAX_TOUCH_POINTS];
        // Whether or not there is at least one finger down (isDown) and/or at
        // least two fingers down (isMultiTouch)
        private boolean isDown, isMultiTouch;        private float[] pressures = new float[MAX_TOUCH_POINTS];
        // Whether or not these fields have already been calculated, for caching
        // purposes
        private boolean diameterSqIsCalculated, diameterIsCalculated,
                angleIsCalculated;        private int[] pointerIds = new int[MAX_TOUCH_POINTS];
        // Event action code and event time
        private int action;
        private long eventTime;

        /**
         * Set all point info
         */
        private void set(int numPoints, float[] x, float[] y, float[] pressure,
                         int[] pointerIds, int action, boolean isDown, long eventTime) {
            if (DEBUG)
                Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0]
                        + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0])
                        + " " + (numPoints > 1 ? y[1] : y[0]) + " " + action
                        + " " + isDown);
            this.eventTime = eventTime;
            this.action = action;
            this.numPoints = numPoints;
            for (int i = 0; i < numPoints; i++) {
                this.xs[i] = x[i];
                this.ys[i] = y[i];
                this.pressures[i] = pressure[i];
                this.pointerIds[i] = pointerIds[i];
            }
            this.isDown = isDown;
            this.isMultiTouch = numPoints >= 2;

            if (isMultiTouch) {
                xMid = (x[0] + x[1]) * .5f;
                yMid = (y[0] + y[1]) * .5f;
                pressureMid = (pressure[0] + pressure[1]) * .5f;
                dx = Math.abs(x[1] - x[0]);
                dy = Math.abs(y[1] - y[0]);

            } else {
                // Single-touch event
                xMid = x[0];
                yMid = y[0];
                pressureMid = pressure[0];
                dx = dy = 0.0f;
            }
            // Need to re-calculate the expensive params if they're needed
            diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false;
        }

        /**
         * Copy all fields from one PointInfo class to another. PointInfo
         * objects are volatile so you should use this if you want to keep track
         * of the last touch event in your own code.
         */
        public void set(PointInfo other) {
            this.numPoints = other.numPoints;
            for (int i = 0; i < numPoints; i++) {
                this.xs[i] = other.xs[i];
                this.ys[i] = other.ys[i];
                this.pressures[i] = other.pressures[i];
                this.pointerIds[i] = other.pointerIds[i];
            }
            this.xMid = other.xMid;
            this.yMid = other.yMid;
            this.pressureMid = other.pressureMid;
            this.dx = other.dx;
            this.dy = other.dy;
            this.diameter = other.diameter;
            this.diameterSq = other.diameterSq;
            this.angle = other.angle;
            this.isDown = other.isDown;
            this.action = other.action;
            this.isMultiTouch = other.isMultiTouch;
            this.diameterIsCalculated = other.diameterIsCalculated;
            this.diameterSqIsCalculated = other.diameterSqIsCalculated;
            this.angleIsCalculated = other.angleIsCalculated;
            this.eventTime = other.eventTime;
        }

        /**
         * True if number of touch points >= 2.
         */
        public boolean isMultiTouch() {
            return isMultiTouch;
        }

        /**
         * Difference between x coords of touchpoint 0 and 1.
         */
        public float getMultiTouchWidth() {
            return isMultiTouch ? dx : 0.0f;
        }

        // -------------------------------------------------------------------------------------------------------------------------------------------

        /**
         * Difference between y coords of touchpoint 0 and 1.
         */
        public float getMultiTouchHeight() {
            return isMultiTouch ? dy : 0.0f;
        }

        /**
         * Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() for
         * integers.
         */
        private int julery_isqrt(int val) {
            int temp, g = 0, b = 0x8000, bshft = 15;
            do {
                if (val >= (temp = (((g << 1) + b) << bshft--))) {
                    g += b;
                    val -= temp;
                }
            } while ((b >>= 1) > 0);
            return g;
        }

        // -------------------------------------------------------------------------------------------------------------------------------------------

        /**
         * Calculate the squared diameter of the multitouch event, and cache it.
         * Use this if you don't need to perform the sqrt.
         */
        public float getMultiTouchDiameterSq() {
            if (!diameterSqIsCalculated) {
                diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f);
                diameterSqIsCalculated = true;
            }
            return diameterSq;
        }

        /**
         * Calculate the diameter of the multitouch event, and cache it. Uses
         * fast int sqrt but gives accuracy to 1/16px.
         */
        public float getMultiTouchDiameter() {
            if (!diameterIsCalculated) {
                if (!isMultiTouch) {
                    diameter = 0.0f;
                } else {
                    // Get 1/16 pixel's worth of subpixel accuracy, works on
                    // screens up to 2048x2048
                    // before we get overflow (at which point you can reduce or
                    // eliminate subpix
                    // accuracy, or use longs in julery_isqrt())
                    float diamSq = getMultiTouchDiameterSq();
                    diameter = (diamSq == 0.0f ? 0.0f
                            : julery_isqrt((int) (256 * diamSq)) / 16.0f);
                    // Make sure diameter is never less than dx or dy, for trig
                    // purposes
                    if (diameter < dx)
                        diameter = dx;
                    if (diameter < dy)
                        diameter = dy;
                }
                diameterIsCalculated = true;
            }
            return diameter;
        }

        /**
         * Calculate the angle of a multitouch event, and cache it. Actually
         * gives the smaller of the two angles between the x axis and the line
         * between the two touchpoints, so range is [0,Math.PI/2]. Uses
         * Math.atan2().
         */
        public float getMultiTouchAngle() {
            if (!angleIsCalculated) {
                if (!isMultiTouch)
                    angle = 0.0f;
                else
                    angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]);
                angleIsCalculated = true;
            }
            return angle;
        }

        /**
         * Return the total number of touch points
         */
        public int getNumTouchPoints() {
            return numPoints;
        }

        /**
         * Return the X coord of the first touch point if there's only one, or
         * the midpoint between first and second touch points if two or more.
         */
        public float getX() {
            return xMid;
        }

        /**
         * Return the array of X coords -- only the first getNumTouchPoints() of
         * these is defined.
         */
        public float[] getXs() {
            return xs;
        }

        /**
         * Return the X coord of the first touch point if there's only one, or
         * the midpoint between first and second touch points if two or more.
         */
        public float getY() {
            return yMid;
        }

        // -------------------------------------------------------------------------------------------------------------------------------------------

        /**
         * Return the array of Y coords -- only the first getNumTouchPoints() of
         * these is defined.
         */
        public float[] getYs() {
            return ys;
        }

        /**
         * Return the array of pointer ids -- only the first getNumTouchPoints()
         * of these is defined. These don't have to be all the numbers from 0 to
         * getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger
         * is lifted and the touch sensor is capable of detecting that that
         * particular touch point is no longer down. Note that a lot of sensors
         * do not have this capability: when finger 1 is lifted up finger 2
         * becomes the new finger 1. However in theory these IDs can correct for
         * that. Convert back to indices using MotionEvent.findPointerIndex().
         */
        public int[] getPointerIds() {
            return pointerIds;
        }

        /**
         * Return the pressure the first touch point if there's only one, or the
         * average pressure of first and second touch points if two or more.
         */
        public float getPressure() {
            return pressureMid;
        }

        /**
         * Return the array of pressures -- only the first getNumTouchPoints()
         * of these is defined.
         */
        public float[] getPressures() {
            return pressures;
        }

        public boolean isDown() {
            return isDown;
        }

        public int getAction() {
            return action;
        }

        public long getEventTime() {
            return eventTime;
        }



        // -------------------------------------------------------------------------------------------------------------------------------------------






    }

    // ------------------------------------------------------------------------------------

    /**
     * A class that is used to store scroll offsets and scale information for
     * objects that are managed by the multitouch controller
     */
    public static class PositionAndScale {
        private float xOff, yOff, scale, scaleX, scaleY, angle;
        private boolean updateScale, updateScaleXY, updateAngle;

        /**
         * Set position and optionally scale, anisotropic scale, and/or angle.
         * Where if the corresponding "update" flag is set to false, the field's
         * value will not be changed during a pinch operation. If the value is
         * not being updated *and* the value is not used by the client
         * application, then the value can just be zero. However if the value is
         * not being updated but the value *is* being used by the client
         * application, the value should still be specified and the update flag
         * should be false (e.g. angle of the object being dragged should still
         * be specified even if the program is in "resize" mode rather than
         * "rotate" mode).
         */
        public void set(float xOff, float yOff, boolean updateScale,
                        float scale, boolean updateScaleXY, float scaleX, float scaleY,
                        boolean updateAngle, float angle) {
            this.xOff = xOff;
            this.yOff = yOff;
            this.updateScale = updateScale;
            this.scale = scale == 0.0f ? 1.0f : scale;
            this.updateScaleXY = updateScaleXY;
            this.scaleX = scaleX == 0.0f ? 1.0f : scaleX;
            this.scaleY = scaleY == 0.0f ? 1.0f : scaleY;
            this.updateAngle = updateAngle;
            this.angle = angle;
        }

        /**
         * Set position and optionally scale, anisotropic scale, and/or angle,
         * without changing the "update" flags.
         */
        protected void set(float xOff, float yOff, float scale, float scaleX,
                           float scaleY, float angle) {
            this.xOff = xOff;
            this.yOff = yOff;
            this.scale = scale == 0.0f ? 1.0f : scale;
            this.scaleX = scaleX == 0.0f ? 1.0f : scaleX;
            this.scaleY = scaleY == 0.0f ? 1.0f : scaleY;
            this.angle = angle;
        }

        public float getXOff() {
            return xOff;
        }

        public float getYOff() {
            return yOff;
        }

        public float getScale() {
            return !updateScale ? 1.0f : scale;
        }

        /**
         * Included in case you want to support anisotropic scaling
         */
        public float getScaleX() {
            return !updateScaleXY ? 1.0f : scaleX;
        }

        /**
         * Included in case you want to support anisotropic scaling
         */
        public float getScaleY() {
            return !updateScaleXY ? 1.0f : scaleY;
        }

        public float getAngle() {
            return !updateAngle ? 0.0f : angle;
        }
    }
}
